Skip to content

Refactor: Profiles Grid, Profile Management (Reorder/Rename/Delete), and Search Improvements#386

Open
ALE-ARME wants to merge 5 commits intoyogeshpaliyal:masterfrom
ALE-ARME:issue-360-partial
Open

Refactor: Profiles Grid, Profile Management (Reorder/Rename/Delete), and Search Improvements#386
ALE-ARME wants to merge 5 commits intoyogeshpaliyal:masterfrom
ALE-ARME:issue-360-partial

Conversation

@ALE-ARME
Copy link
Copy Markdown
Contributor

@ALE-ARME ALE-ARME commented Feb 10, 2026

This Pull Request implements significant UI/UX enhancements focused on profile management and search functionality, resolving various data integrity and navigation issues.

🎩 Key Changes:

  • Profiles Grid: Replaced the default Home screen with a modern grid view for profiles.
  • Profile Management:
    • Long-press: Added management options to Rename or Delete profiles via long-press.
    • Reordering (Fixes Manually reorder list of profiles #385): Integrated a persistent priority system with UI controls (Up/Down arrows) to custom-order profiles.
    • Styling: Added a profile counter in the TopAppBar and centered the deletion dialog with a red warning icon and bold names.
  • Data Integrity: Enabled SQLite Foreign Key constraints to ensure ON DELETE CASCADE correctly removes all associated links when a profile is deleted.
  • Navigation Improvements:
    • Updated Bottom Bar icon and label from Home to Profiles.
    • Back Navigation: The system back button now correctly returns users to the Profiles Grid when viewing a specific profile.
  • Unified Search: The search bar now filters both links and tags simultaneously within the active profile.
  • General UI Polish:
    • Centered and styled the Tag deletion dialog to match the profile management UI.
    • Removed trailing dots from various informational sentences for consistency across the app.
  • Documentation: Updated README.md with instructions for new navigation and profile management features.

🔗 Link Pre-population:

  • The old automatic clipboard detection system has been removed from both code and settings.
  • New behavior: Users must now long-press the + (Add Link) button to open the creation screen with the clipboard link auto-populated.
  • Fixed: Auto-population is now fully functional, using direct clipboard reading and resilient state management to ensure the link is correctly captured.

Fixes #360 (Partial)
Fixes #385

Summary by CodeRabbit

Release Notes

  • New Features

    • Profile management grid with drag-to-reorder functionality
    • Localized web interface supporting multiple languages
    • Enhanced link metadata auto-population
  • Improvements

    • QR code display with improved black-on-white contrast
    • Theme selection now localized
    • Streamlined settings by removing clipboard detection
  • Documentation

    • Updated build variant feature matrix in README

@ALE-ARME
Copy link
Copy Markdown
Contributor Author

ALE-ARME commented Feb 10, 2026

If AI didn't mess up this PR should contain only partial fixes of the Home page improvements issue and the one about profile reordering.
The ones with strike though in the issue are the one present here. I'll leave the rest to you

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This PR removes a legacy APK build workflow, introduces profile priority-based reordering with database schema changes, adds a profiles grid UI for managing profiles, enhances clipboard link detection with DisposableEffect, customizes QR code colors, adds web server localization, removes clipboard UI settings, and extends string resources for new profile and web interface features.

Changes

Cohort / File(s) Summary
CI/CD Workflow Removal
.github/workflows/master-apk.yml
Removed GitHub Actions workflow for automated APK builds from master branch.
Documentation
README.md
Updated build variant feature table column order (moved F-Droid before Play Store) and expanded profile navigation description; removed trailing periods from UI strings for consistency.
Database Schema & Priority
app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq
Added priority column to Profile table with default 0; updated insertProfile to accept priority parameter; modified getAllProfiles to order by priority ASC then createdAt ASC; added updateProfilePriority and maxPriority queries.
Repository Layer
app/src/main/java/com/yogeshpaliyal/deepr/data/LinkRepository.kt, LinkRepositoryImpl.kt, backup/importer/CsvBookmarkImporter.kt
Updated insertProfile to accept priority parameter with default 0; added updateProfilePriority and getMaxPriority methods; extended getLinksAndTags with searchQuery4 parameter.
Database Initialization
app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt
Switched AndroidSqliteDriver to named parameters and added callback to enable foreign key constraints on database open.
Activity & Clipboard
app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt
Replaced LaunchedEffect with DisposableEffect for clipboard link detection using OnPrimaryClipChangedListener; added initial clipboard check; integrated profile grid display trigger on Dashboard2 navigation.
QR Code Styling
app/src/main/java/com/yogeshpaliyal/deepr/ui/components/QRCodeDialog.kt, ui/screens/LocalNetworkServer.kt, ui/screens/TransferLinkLocalServer.kt
Hard-coded QR code colors to white background and black foreground; removed dynamic theming imports; applied color customization across dialog and server screens.
Theme & Settings
app/src/main/java/com/yogeshpaliyal/deepr/ui/components/ThemeSelectionDialog.kt, ui/screens/Settings.kt
Replaced hard-coded theme strings with stringResource lookups; removed clipboard link detection UI and associated state from Settings screen.
Add Link Screen
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt
Enhanced deeprInfo state initialization with selectedLink keys; centralized metadata fetching in fetchMetadata function; added auto-fetch on link change; replaced inlined fetch logic with shared isFetchingMetadata state; updated placeholders to localized strings.
Profiles & Home
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt
Added ProfilesGrid composable for profile selection with reordering controls; introduced RenameDeleteProfileDialog for profile management; updated Dashboard2 icon to User and label to "Profiles"; integrated profile creation flow with validation.
Tag Management
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt
Centered delete confirmation dialog title and content; switched to annotated string for bolded tag names; replaced hard-coded link count text with stringResource-based singular/plural support; adjusted styling with centered layout modifiers.
ViewModel
app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt
Added showProfilesGrid StateFlow and setShowProfilesGrid setter; implemented priority normalization on init to reorder profiles by index; added moveProfileUp and moveProfileDown methods for profile reordering; updated insertProfile to calculate max priority and insert with max + 1; enhanced allTagsWithCount with searchQuery filtering.
Web Server
app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt
Added HTML localization with placeholder replacement from Android resources; updated API endpoints to pass additional filter parameters for expanded query compatibility; added POST /api/links/increment-count endpoint for link open count increments; updated insertProfile call with explicit priority 0L parameter.
Internationalization
app/src/main/res/values*/strings.xml (de, es, et, fr, hi, it, pl, ru, values)
Removed trailing periods from multiple string resources for consistency; added extensive new string resources for profile management (reorder, profile_delete_confirmation_with_name), web interface (web_title, web_profile_label, web_links, etc.), and miscellaneous UI elements (theme, unknown, preview, link_placeholder); expanded Italian strings file with rephrasings and new localization keys across multiple sections.
Build Trigger
trigger_build.txt
Added build trigger marker file with timestamp.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeUI as Home UI<br/>(ProfilesGrid)
    participant ViewModel as AccountViewModel
    participant Repo as LinkRepository
    participant DB as SQLite Database

    User->>HomeUI: Clicks move up on profile
    HomeUI->>ViewModel: moveProfileUp(profileId)
    
    ViewModel->>ViewModel: normalizePriorities()
    Note over ViewModel: Reorder priorities<br/>to match index
    
    ViewModel->>Repo: updateProfilePriority(neighborId, newPriority)
    Repo->>DB: UPDATE Profile SET priority = ?
    
    ViewModel->>Repo: updateProfilePriority(profileId, neighborPriority)
    Repo->>DB: UPDATE Profile SET priority = ?
    
    ViewModel->>Repo: getAllProfiles()
    Repo->>DB: SELECT * FROM Profile<br/>ORDER BY priority ASC
    DB-->>Repo: Ordered profiles
    Repo-->>ViewModel: Updated profiles
    
    ViewModel-->>HomeUI: Emit updated profiles
    HomeUI-->>User: Display reordered grid
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #365: Modifies localization resource files across multiple language variants (values-it, values-es, values-et, etc.), sharing similar string resource update patterns and scope with this PR's extensive internationalization changes.

Poem

🐰 Profiles dance in ordered rows,
Priorities shuffled to and fro,
With grids so fine and reordering grace,
Each profile finds its proper place! 🎪

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes align with linked issues #360 and #385; however, several modifications appear tangential: QR code color hardcoding changes in QRCodeDialog.kt, LocalNetworkServer.kt, and TransferLinkLocalServer.kt are not explicitly mentioned in issue requirements; trigger_build.txt addition appears unrelated to feature requirements; settings file modifications primarily remove UI rather than implementing features. Clarify whether QR code styling changes and trigger_build.txt are intentional or accidental additions that should be reverted or documented separately.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Refactor: Profiles Grid, Profile Management (Reorder/Rename/Delete), and Search Improvements' accurately and specifically describes the main changes: implementing a profiles grid UI, adding profile management features (reorder/rename/delete), and search improvements.
Linked Issues check ✅ Passed The PR addresses requirements from linked issues #360 and #385: implements profiles grid as primary landing view [#360], enables profile reordering with priority system [#385], adds rename/delete dialogs [#360], enables foreign key constraints [#385], updates navigation bar icons/labels [#360], implements unified search filtering links and tags [#360], removes clipboard detection UI [#360], removes trailing punctuation [#360], and updates README with navigation behavior [#360].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt (2)

176-195: ⚠️ Potential issue | 🟡 Minor

toLong() on comma-split tag IDs can throw if data is malformed.

Line 181: tagId.trim().toLong() will throw NumberFormatException if a tag ID is empty or non-numeric (e.g., trailing comma in tagsIds). Consider using toLongOrNull() with a filter to silently skip invalid entries.

Suggested defensive parsing
             val existingTags =
-                selectedLink.tagsIds?.split(",")?.mapIndexed { index, tagId ->
-                    Tags(
-                        tagId.trim().toLong(),
+                selectedLink.tagsIds?.split(",")?.mapIndexedNotNull { index, tagId ->
+                    val id = tagId.trim().toLongOrNull() ?: return@mapIndexedNotNull null
+                    Tags(
+                        id,
                         selectedLink.tagsNames
                             ?.split(",")
                             ?.getOrNull(index)
                             ?.trim() ?: context.getString(R.string.unknown),
                     )
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`
around lines 176 - 195, The parsing of comma-separated tag IDs in the
LaunchedEffect (inside AddLinkScreen.kt) is unsafe because tagId.trim().toLong()
can throw; update the block that builds existingTags to use safe parsing (e.g.,
use toLongOrNull() and mapIndexedNotNull) so invalid or empty IDs are skipped,
while still pairing each successful Long with its corresponding name from
selectedLink.tagsNames?.split(",")?.getOrNull(index)?.trim() (falling back to
context.getString(R.string.unknown) when name is missing); then populate
selectedTags and initialSelectedTags with the filtered Tags objects to avoid
exceptions on malformed tagsIds.

833-909: ⚠️ Potential issue | 🟠 Major

Duplicate-link checks use the raw (non-normalized) link, diverging from save() which normalizes first.

The "Save & Execute" (line 837) and "Save" (line 871) buttons call deeprQueries.getDeeprByLink(deeprInfo.link) with the raw user input, while the save lambda (line 199) normalizes via normalizeLink() before the same check. This means a user entering example.com will pass the button-level guard (DB has https://example.com, no match for example.com), then save() normalizes, finds the duplicate, shows a toast, and returns — without executing even if "Save & Execute" was clicked.

Additionally, the validation+duplicate logic is repeated three times with subtle differences (executeAsOneOrNull in save vs executeAsList().isNotEmpty() in the buttons). Consider extracting a single validation helper that normalizes then checks, and call it from all paths.

Proposed refactor: extract shared validation and normalize consistently
     val save: (executeAfterSave: Boolean) -> Unit = save@{ executeAfterSave ->
-        // Normalize the link before saving
         val normalizedLink = normalizeLink(deeprInfo.link)
-
-        if (isCreate && deeprQueries.getDeeprByLink(normalizedLink).executeAsOneOrNull() != null) {
-            Toast.makeText(context, deeplinkExistsText, Toast.LENGTH_SHORT).show()
-            return@save
-        }
         // ... rest of save logic
     }
+
+    val validateAndSave: (executeAfterSave: Boolean) -> Unit = { executeAfterSave ->
+        val normalizedLink = normalizeLink(deeprInfo.link)
+        if (!isValidDeeplink(normalizedLink)) {
+            isError = true
+        } else if (isCreate && deeprQueries.getDeeprByLink(normalizedLink).executeAsList().isNotEmpty()) {
+            Toast.makeText(context, deeplinkExistsText, Toast.LENGTH_SHORT).show()
+        } else {
+            save(executeAfterSave)
+        }
+    }

Then replace all three button onClick blocks with validateAndSave(true) / validateAndSave(false).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`
around lines 833 - 909, The button handlers use the raw deeprInfo.link and call
deeprQueries.getDeeprByLink(...) directly, but save() first normalizes the link
via normalizeLink() and uses executeAsOneOrNull(), causing inconsistent
duplicate detection and duplicated logic; create a single helper e.g.
validateAndSave(execute: Boolean) that calls normalizeLink(deeprInfo.link), then
queries deeprQueries.getDeeprByLink(normalizedLink) using the same existence
check as save (executeAsOneOrNull or a consistent API), sets isError on invalid
links, shows the duplicate Toast when found, and calls save(execute) (or
openDeeplink for execute-only flows) as appropriate; replace the three onClick
blocks (the Button and two FilledTonalButton handlers referencing
deeprInfo.link, save(true)/save(false), and openDeeplink) to call
validateAndSave(true/false) so normalization and duplicate-checking are
centralized and consistent.
app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt (1)

200-233: ⚠️ Potential issue | 🟠 Major

getPrimaryClip() inside OnPrimaryClipChangedListener triggers an Android 12+ system toast on every clipboard change

On Android 12 and higher, when an app calls getPrimaryClip() to access clip data from a different app for the first time, a toast message notifies the user of this clipboard access. On Android 12 (API level 31) and higher, the system usually shows a toast message when your app calls getPrimaryClip().

Because clipboard.primaryClip is called unconditionally inside the OnPrimaryClipChangedListener callback, the system will show a "Deepr pasted from your clipboard" toast every time the user copies anything (passwords, addresses, random text) while this app is running — regardless of whether the content is a deeplink. Using getPrimaryClipDescription() avoids this: data is not copied and the toast message is not displayed.

The fix is to gate the full getPrimaryClip() call on the MIME type: use getPrimaryClipDescription() first (no toast) to confirm the clip is plain text, then call getPrimaryClip() only if warranted.

🛡️ Proposed fix to avoid spurious clipboard-access toasts on Android 12+
 val listener = ClipboardManager.OnPrimaryClipChangedListener {
-    val clipData = clipboard.primaryClip
-    if (clipData != null && clipData.itemCount > 0) {
-        val text = clipData.getItemAt(0).text?.toString()
-        if (!text.isNullOrBlank()) {
-            val normalizedLink = normalizeLink(text)
-            if (isValidDeeplink(normalizedLink)) {
-                clipboardLink = ClipboardLink(normalizedLink)
-            }
-        }
-    }
+    val description = clipboard.primaryClipDescription
+    if (description != null && description.hasMimeType(android.content.ClipDescription.MIMETYPE_TEXT_PLAIN)) {
+        val clipData = clipboard.primaryClip
+        if (clipData != null && clipData.itemCount > 0) {
+            val text = clipData.getItemAt(0).text?.toString()
+            if (!text.isNullOrBlank()) {
+                val normalizedLink = normalizeLink(text)
+                if (isValidDeeplink(normalizedLink)) {
+                    clipboardLink = ClipboardLink(normalizedLink)
+                }
+            }
+        }
+    }
 }

 // Initial check
-val clipData = clipboard.primaryClip
-if (clipData != null && clipData.itemCount > 0) {
-    val text = clipData.getItemAt(0).text?.toString()
-    if (!text.isNullOrBlank()) {
-        val normalizedLink = normalizeLink(text)
-        if (isValidDeeplink(normalizedLink)) {
-            clipboardLink = ClipboardLink(normalizedLink)
-        }
-    }
-}
+val description = clipboard.primaryClipDescription
+if (description != null && description.hasMimeType(android.content.ClipDescription.MIMETYPE_TEXT_PLAIN)) {
+    val clipData = clipboard.primaryClip
+    if (clipData != null && clipData.itemCount > 0) {
+        val text = clipData.getItemAt(0).text?.toString()
+        if (!text.isNullOrBlank()) {
+            val normalizedLink = normalizeLink(text)
+            if (isValidDeeplink(normalizedLink)) {
+                clipboardLink = ClipboardLink(normalizedLink)
+            }
+        }
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt` around lines 200 -
233, The clipboard listener calls clipboard.primaryClip unconditionally which
triggers Android 12+ clipboard-access toasts; change both the listener body and
the initial check in the DisposableEffect to first call
clipboard.primaryClipDescription (or getPrimaryClipDescription()) and verify the
MIME type is text/plain (or hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN))
before calling clipboard.primaryClip/getItemAt(0); keep using normalizeLink,
isValidDeeplink and ClipboardLink as before, and ensure you only call
clipboard.primaryClip when the description indicates text to avoid spurious
toasts.
app/src/main/res/values/strings.xml (1)

267-267: ⚠️ Potential issue | 🟡 Minor

Remove unused profile_name_cannot_be_empty string resource

profile_name_cannot_be_empty (line 344) is not referenced anywhere in the codebase—only profile_name_cannot_be_blank is actively used. The unused key creates unnecessary translation maintenance burden (currently defined in only 2 locales: values and values-it) and should be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values/strings.xml` at line 267, Remove the unused string
resource named profile_name_cannot_be_empty: delete its entry from the default
strings.xml and the corresponding translation in values-it/strings.xml, and
verify there are no references (grep/search for "profile_name_cannot_be_empty")
before committing; keep the active string profile_name_cannot_be_blank intact.
app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt (1)

309-347: ⚠️ Potential issue | 🟠 Major

N+1 query pattern in GET /api/tags with hardcoded profileId = 1L

For every tag in the database, a full getLinksAndTags(...) query is executed and .size is called on the result set. This loads entire link rows just to count them — O(n_tags × m_links) in terms of DB work and memory allocation.

The repository already exposes getAllTagsWithCount(profileId) which is a single, optimized SQL query designed for exactly this use case. The profileId is also hardcoded to 1L here, which will return wrong counts for users who have links in other profiles.

♻️ Proposed fix
 get("/api/tags") {
     try {
+        val profileId = call.request.queryParameters["profileId"]?.toLongOrNull() ?: 1L
-        val allTags = deeprQueries.getAllTags().executeAsList()
-        val response = allTags.map { tag ->
-            val linkCount = deeprQueries.getLinksAndTags(
-                1L, "", "", "", "", -1L, -1L,
-                tag.id.toString(), tag.id.toString(),
-                "DESC", "createdAt", "DESC", "createdAt",
-            ).executeAsList().size
-            TagResponse(id = tag.id, name = tag.name, count = linkCount)
-        }
+        val tagsWithCount = deeprQueries.getAllTagsWithCount(profileId).executeAsList()
+        val response = tagsWithCount.map { row ->
+            TagResponse(id = row.id, name = row.name, count = row.linkCount.toInt())
+        }
         call.respond(HttpStatusCode.OK, response)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`
around lines 309 - 347, The GET /api/tags handler is performing an N+1 query by
calling deeprQueries.getLinksAndTags(...) for each tag and hardcodes profileId =
1L; replace that loop with the optimized single-query method
deeprQueries.getAllTagsWithCount(profileId) to return tags with counts in one
call, and obtain the correct profileId from the request/context (instead of 1L).
Map the results of getAllTagsWithCount(...) to TagResponse(id, name, count) and
respond with that list; remove the per-tag getLinksAndTags(...) calls and their
.size usage to eliminate the N+1 issue and ensure counts respect the actual
profileId.
🧹 Nitpick comments (14)
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt (1)

139-145: Auto-fetch has no cancellation of in-flight requests and no debounce.

fetchMetadata is a plain lambda (not a suspend function), so when deeprInfo.link changes and LaunchedEffect restarts, the previously fired viewModel.fetchMetaData() call is not cancelled. If the link changes rapidly (e.g., QR scan immediately followed by a manual edit), stale callbacks can overwrite the current metadata.

Consider making fetchMetaData a suspend function invoked directly inside the LaunchedEffect coroutine (so it's automatically cancelled on re-key), or track/ignore stale responses by comparing the link at callback time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`
around lines 139 - 145, The LaunchedEffect block starts new work on
deeprInfo.link but calls a non-cancellable callback (fetchMetadata /
viewModel.fetchMetaData), causing stale responses to overwrite newer state;
change the implementation so the metadata fetch is cancellable or ignores stale
results: either convert fetchMetadata (and underlying viewModel.fetchMetaData)
into a suspend function and call it directly inside the LaunchedEffect coroutine
(so coroutine cancellation will abort in-flight work), or add a request token
(capture the current normalized link or a UUID in LaunchedEffect) and have the
fetch callback verify that the token/link still matches before applying results
(compare against deeprInfo.link or the captured token in the callback). Ensure
you update references to normalizeLink, isValidDeeplink, fetchMetadata, and
viewModel.fetchMetaData accordingly so only the latest request can update UI.
app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt (2)

200-200: Missing DisposableEffect import; stale LaunchedEffect import

DisposableEffect is invoked via its fully qualified name (androidx.compose.runtime.DisposableEffect) rather than being imported like every other Compose API in this file. At the same time, the old import androidx.compose.runtime.LaunchedEffect at line 24 appears unused after this refactor.

♻️ Proposed fix
+import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect

Then on line 200:

-    androidx.compose.runtime.DisposableEffect(Unit) {
+    DisposableEffect(Unit) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt` at line 200, File
uses androidx.compose.runtime.DisposableEffect with a fully-qualified name and
still has an unused import for LaunchedEffect; replace the fully-qualified call
with a proper import and remove the stale import. Add "import
androidx.compose.runtime.DisposableEffect" to the imports, remove "import
androidx.compose.runtime.LaunchedEffect" if unused, and update any usages (e.g.,
the call currently written as androidx.compose.runtime.DisposableEffect(Unit) {
... }) to use the imported DisposableEffect symbol so imports are consistent and
no unused LaunchedEffect remains.

203-226: Extract duplicated clipboard-reading logic into a shared helper lambda

The listener body (lines 203–213) and the initial check (lines 217–226) are byte-for-byte identical. Even after applying the getPrimaryClipDescription() guard above, both blocks will remain duplicated.

♻️ Proposed refactor
+    val processClip = {
+        val description = clipboard.primaryClipDescription
+        if (description != null && description.hasMimeType(android.content.ClipDescription.MIMETYPE_TEXT_PLAIN)) {
+            val clipData = clipboard.primaryClip
+            if (clipData != null && clipData.itemCount > 0) {
+                val text = clipData.getItemAt(0).text?.toString()
+                if (!text.isNullOrBlank()) {
+                    val normalizedLink = normalizeLink(text)
+                    if (isValidDeeplink(normalizedLink)) {
+                        clipboardLink = ClipboardLink(normalizedLink)
+                    }
+                }
+            }
+        }
+    }
+
-    val listener = ClipboardManager.OnPrimaryClipChangedListener {
-        val clipData = clipboard.primaryClip
-        ...
-    }
+    val listener = ClipboardManager.OnPrimaryClipChangedListener { processClip() }

-    // Initial check
-    val clipData = clipboard.primaryClip
-    ...
+    // Initial check
+    processClip()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt` around lines 203 -
226, The clipboard-reading logic is duplicated between the
ClipboardManager.OnPrimaryClipChangedListener block and the initial check;
extract that shared code into a single helper (either a private function or a
lambda) — e.g., create a processClipboard() function that reads
clipboard.primaryClip, gets text, calls normalizeLink(text), checks
isValidDeeplink(normalizedLink) and assigns clipboardLink =
ClipboardLink(normalizedLink) — then call that helper from the listener body and
from the initial check so ClipboardManager.OnPrimaryClipChangedListener,
normalizeLink, isValidDeeplink, ClipboardLink and clipboard are reused without
duplication.
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt (1)

45-45: Optional: import QrCodeColors instead of using its fully qualified name.

QrCodeView is imported at Line 59, but QrCodeColors is referenced with its fully qualified class name inline. Adding an import keeps the call-site consistent and easier to read.

♻️ Proposed change
 import com.lightspark.composeqr.QrCodeView
+import com.lightspark.composeqr.QrCodeColors
-                colors =
-                    com.lightspark.composeqr.QrCodeColors(
-                        background = Color.White,
-                        foreground = Color.Black,
-                    ),
+                colors =
+                    QrCodeColors(
+                        background = Color.White,
+                        foreground = Color.Black,
+                    ),

Also applies to: 323-327

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt`
at line 45, Replace the fully-qualified QrCodeColors usages with a simple
import: add an import for QrCodeColors at the top of the file and update all
call-sites (the places where QrCodeView is used and the occurrences around the
previous lines 323-327) to reference QrCodeColors directly instead of its
fully-qualified name; this keeps the import style consistent with QrCodeView and
improves readability.
app/src/main/java/com/yogeshpaliyal/deepr/ui/components/NoteViewDialog.kt (1)

91-104: Migrate from deprecated ClickableText to BasicText with LinkAnnotation.

ClickableText is deprecated as of Compose Foundation 1.7.0-alpha07, and your project's Compose BOM (2025.09.00) is well past this point. Replace the StringAnnotation("URL", ...) offset-lookup pattern with BasicText + LinkAnnotation.Clickable using withLink() in buildAnnotatedString. Pass LinkInteractionListener to LinkAnnotation.Clickable to retain the custom openDeeplink() dispatch.

Migration sketch
-import androidx.compose.foundation.text.ClickableText
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.withLink
 val annotatedString = buildAnnotatedString {
     val text = deepr.notes
-    append(text)
     val matcher = Patterns.WEB_URL.matcher(text)
+    var cursor = 0
     while (matcher.find()) {
+        append(text.substring(cursor, matcher.start()))
         val url = text.substring(matcher.start(), matcher.end())
-        addStringAnnotation(tag = "URL", annotation = url, start = matcher.start(), end = matcher.end())
-        addStyle(SpanStyle(color = primaryColor, textDecoration = TextDecoration.Underline), start = matcher.start(), end = matcher.end())
+        withLink(
+            LinkAnnotation.Clickable(
+                tag = url,
+                styles = TextLinkStyles(SpanStyle(color = primaryColor, textDecoration = TextDecoration.Underline)),
+                linkInteractionListener = { openDeeplink(context, url) }
+            )
+        ) { append(url) }
+        cursor = matcher.end()
     }
+    append(text.substring(cursor))
 }

 SelectionContainer {
-    ClickableText(
+    BasicText(
         text = annotatedString,
         style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
-        onClick = { offset ->
-            annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
-                .firstOrNull()?.let { openDeeplink(context, it.item) }
-        },
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/components/NoteViewDialog.kt`
around lines 91 - 104, Replace the deprecated ClickableText usage by building
annotatedString with LinkAnnotation.Clickable via withLink() in
buildAnnotatedString and render it with BasicText; update the annotation
creation code where you currently use StringAnnotation("URL", ...) so it uses
withLink(url) / LinkAnnotation.Clickable and attach a LinkInteractionListener
that calls openDeeplink(context, url) (preserve the existing annotatedString
variable name and the openDeeplink dispatch logic), then replace the
ClickableText block (which performs getStringAnnotations and offset lookup) with
a BasicText that uses the new annotatedString and the LinkAnnotation.Clickable
listener to handle taps.
app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq (1)

218-220: Stray blank line between GROUP BY and ORDER BY is inconsistent with all other queries in this file.

SQLDelight's parser treats this as harmless whitespace, but every other multi-line query in Deepr.sq has no blank lines mid-body. Remove line 219 for consistency.

♻️ Proposed cleanup
 GROUP BY Tags.id, Tags.name
-
 ORDER BY linkCount DESC, Tags.name;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq` around lines 218 -
220, Remove the stray blank line between the GROUP BY clause and the ORDER BY
clause so the query remains consistent with others in the file; specifically,
join "GROUP BY Tags.id, Tags.name" and "ORDER BY linkCount DESC, Tags.name" with
no blank line between them (the relevant symbols to locate are GROUP BY,
Tags.id, Tags.name, ORDER BY, and linkCount).
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt (1)

692-709: split(tag.name) loses trailing content when the name occurs more than once in the formatted string.

If the user somehow names a tag with a value that appears multiple times in the resolved format string, parts.size > 2 and only parts[0] + bold + parts[1] is rendered — content after the second occurrence is silently dropped. While the current delete_tag_confirmation_with_name has a single %s, this is fragile for future copy changes. Using indexOf is more robust.

♻️ Robust AnnotatedString construction via indexOf
-val fullMessage = stringResource(R.string.delete_tag_confirmation_with_name, tag.name)
-val parts = fullMessage.split(tag.name)
-val annotatedString = buildAnnotatedString {
-    if (parts.size >= 2) {
-        append(parts[0])
-        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
-            append(tag.name)
-        }
-        append(parts[1])
-    } else {
-        append(fullMessage)
-    }
-}
+val fullMessage = stringResource(R.string.delete_tag_confirmation_with_name, tag.name)
+val annotatedString = buildAnnotatedString {
+    val startIndex = fullMessage.indexOf(tag.name)
+    if (startIndex != -1) {
+        append(fullMessage.substring(0, startIndex))
+        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
+            append(tag.name)
+        }
+        append(fullMessage.substring(startIndex + tag.name.length))
+    } else {
+        append(fullMessage)
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt`
around lines 692 - 709, The current construction in TagSelectionScreen.kt builds
annotatedString by splitting fullMessage with tag.name (variables fullMessage,
tag.name, parts, annotatedString), which drops content after the second
occurrence; instead find the first occurrence using indexOf(tag.name), then
append substring before, append the tag name with bold SpanStyle, and append the
remainder (substring after first match) so any further occurrences are
preserved; update buildAnnotatedString usage accordingly and keep the Text(...)
rendering as-is.
app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt (2)

107-107: Redundant val port = port — no-op self-assignment

This creates a local val that shadows the function parameter with its own value. It serves no purpose and may generate a compiler warning.

✏️ Proposed fix
-        val port = port
-
         server = embeddedServer(CIO, host = "0.0.0.0", port = port) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`
at line 107, Remove the redundant local shadowing of the parameter by deleting
the line "val port = port" in LocalServerRepositoryImpl (the local variable
shadows the function parameter named port); just use the existing parameter
"port" directly everywhere in the function (or rename if you intended a
different variable), ensuring no other code relies on that local declaration.

386-402: POST /api/links/increment-count reads the resource ID from a query parameter

Using a query parameter for a mutable state change on a POST endpoint is non-idiomatic (query params belong on GET/safe methods). The conventional REST design is either POST /api/links/{id}/increment-count (path parameter) or PATCH /api/links/{id} with a body. This also makes the endpoint easier to document and harder to accidentally call via a GET proxy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`
around lines 386 - 402, Change the POST handler that currently reads the link id
from call.request.queryParameters in the route "/api/links/increment-count" to
accept the id as a path parameter (e.g. POST "/api/links/{id}/increment-count")
or accept a JSON body with an id, then parse that value instead of using
queryParameters; update the route declaration and replace
call.request.queryParameters["id"]?.toLongOrNull() with code that retrieves the
id from call.parameters["id"] or from the request body, and continue calling
accountViewModel.incrementOpenedCount(id) and responding with
SuccessResponse/ErrorResponse as before.
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt (3)

1596-1607: Fragile annotated-string construction: split(profile.name) can break for certain names or locales.

If profile.name is a short/common substring (e.g., "a", "de"), split may fragment the translated message at unintended positions. Also, if the name appears more than once in the message, parts.size >= 2 is true but parts[1] won't contain the full trailing text.

A more robust approach is to locate the name's position in the formatted string and split only at that known offset:

Proposed fix
-                    val parts = fullMessage.split(profile.name)
-                    val annotatedString = buildAnnotatedString {
-                        if (parts.size >= 2) {
-                            append(parts[0])
-                            withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
-                                append(profile.name)
-                            }
-                            append(parts[1])
-                        } else {
-                            append(fullMessage)
-                        }
-                    }
+                    val annotatedString = buildAnnotatedString {
+                        val nameIndex = fullMessage.indexOf(profile.name)
+                        if (nameIndex >= 0) {
+                            append(fullMessage.substring(0, nameIndex))
+                            withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
+                                append(profile.name)
+                            }
+                            append(fullMessage.substring(nameIndex + profile.name.length))
+                        } else {
+                            append(fullMessage)
+                        }
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt` around
lines 1596 - 1607, The annotated-string construction is fragile because
split(profile.name) can incorrectly fragment translations or handle multiple
occurrences; instead, find the first occurrence index of profile.name in the
formatted fullMessage (use fullMessage.indexOf(profile.name)) and if index >= 0
build the AnnotatedString by taking fullMessage.substring(0, index), append the
bold profile.name, then append fullMessage.substring(index +
profile.name.length), otherwise fall back to appending fullMessage; update the
logic around fullMessage, parts, and annotatedString in Home.kt to use this
index-based splitting so only the intended occurrence is made bold.

1688-1690: Save button enabled check should compare trimmed names for consistent UX.

enabled = newName.isNotBlank() && newName != profile.name allows enabling Save when only leading/trailing whitespace differs (e.g., " Default " vs "Default"), but the onClick handler trims before comparing, resulting in a "name already exists" error after the user clicks.

Proposed fix
-                    enabled = newName.isNotBlank() && newName != profile.name
+                    enabled = newName.isNotBlank() && newName.trim() != profile.name
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt` around
lines 1688 - 1690, The Save button's enabled check uses raw newName and
profile.name, allowing only-whitespace differences to enable the button but
failing later because onClick trims; update the enabled logic to compare trimmed
values instead (use newName.trim().isNotBlank() and compare newName.trim() with
profile.name.trim() or the canonical stored name) so the enabled state matches
the onClick trimming behavior; locate the Save button enabled assignment
(enabled = newName.isNotBlank() && newName != profile.name) and change it to use
trimmed comparisons consistent with the onClick handler.

569-613: FAB tap/long-press via inner pointerInput bypasses the FAB's ripple and accessibility semantics.

The FloatingActionButton(onClick = {}) has an empty click handler while actual tap handling is done via detectTapGestures on the inner Box. This means the FAB's Material ripple effect and accessibility click action may not behave as expected — the inner pointerInput consumes the gesture before the FAB's clickable modifier processes it.

Consider using combinedClickable (from foundation) on the FAB's modifier instead, which natively supports both onClick and onLongClick while preserving ripple and semantics:

Sketch of alternative approach
-            if (!showProfilesGrid) {
-                FloatingActionButton(
-                    onClick = {},
-                ) {
-                    Box(
-                        modifier =
-                            Modifier
-                                .size(56.dp)
-                                .pointerInput(Unit) {
-                                    detectTapGestures(
-                                        onTap = {
-                                            localNavigator.add(AddLinkScreen(createDeeprObject()))
-                                        },
-                                        onLongPress = {
-                                            hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-                                            // ... clipboard logic ...
-                                            localNavigator.add(AddLinkScreen(createDeeprObject(link = linkToPass)))
-                                        },
-                                    )
-                                },
-                        contentAlignment = Alignment.Center,
-                    ) {
-                        Icon(
-                            TablerIcons.Plus,
-                            contentDescription = stringResource(R.string.add_link),
-                        )
-                    }
-                }
+            if (!showProfilesGrid) {
+                FloatingActionButton(
+                    onClick = {
+                        localNavigator.add(AddLinkScreen(createDeeprObject()))
+                    },
+                    modifier = Modifier.combinedClickable(
+                        onClick = {
+                            localNavigator.add(AddLinkScreen(createDeeprObject()))
+                        },
+                        onLongClick = {
+                            hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+                            // ... clipboard logic ...
+                            localNavigator.add(AddLinkScreen(createDeeprObject(link = linkToPass)))
+                        },
+                    ),
+                ) {
+                    Icon(
+                        TablerIcons.Plus,
+                        contentDescription = stringResource(R.string.add_link),
+                    )
+                }
app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt (2)

849-857: getMaxPriority() + insertProfile(name, maxPriority + 1) is not atomic.

If two profile insertions run concurrently, both can read the same max priority and insert with the same value. Low risk since this is UI-driven (single FAB), but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt`
around lines 849 - 857, The current insertProfile in AccountViewModel uses
linkRepository.getMaxPriority() and then linkRepository.insertProfile(name,
maxPriority + 1) separately, which is not atomic and can cause duplicate
priorities under concurrency; change this by adding a single atomic
repository/DAO operation (e.g.,
linkRepository.insertProfileWithNextPriority(name) or a DAO method annotated
with `@Transaction`) that reads the current max priority and inserts the new
profile inside the same transaction, then call that new method from
AccountViewModel.insertProfile instead of calling getMaxPriority and
insertProfile separately; alternatively, if you prefer in-memory serialization,
guard the two calls with a dedicated Mutex/synchronized block in the repository
so the read+insert cannot interleave.

141-148: normalizePriorities() rewrites every profile's priority on every app launch and every move.

On init (line 136), this runs unconditionally for existing profiles, issuing up to N update queries. On each move, it runs again before the swap. For a small number of profiles this is tolerable, but consider tracking whether normalization is actually needed (e.g., a dirty flag or checking if priorities are already sequential) to avoid unnecessary DB writes on every cold start.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt`
around lines 141 - 148, normalizePriorities() currently issues updates for every
profile on init and before moves; change it to detect whether normalization is
required and only perform writes when priorities are out of order or a "dirty"
flag is set. Specifically, in normalizePriorities() (and where it is called)
first load profiles with linkRepository.getAllProfiles().executeAsList() and
check if any profile.priority != index.toLong()—if none mismatch, return early;
alternatively, add a persistent or in-memory dirty flag that is set when
profiles are added/removed/moved (e.g., in your move/swap methods) and only run
updateProfilePriority(profile.id, index.toLong()) when the flag is true, then
clear the flag after successful normalization. Ensure callers (init and move)
use the new check/flag to avoid unconditional DB writes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt`:
- Line 71: Imported profiles are all saved with priority = 0L at the
deeprQueries.insertProfile(it, 0L) call in CsvBookmarkImporter, causing
indeterminate ordering; modify the import transaction so it queries the current
max priority (using the same pattern as AccountViewModel.insertProfile(), i.e.,
call deeprQueries.maxPriority() or equivalent) before inserting each new
profile, set the profile's priority to maxPriority + 1, and increment that in
the loop for subsequent inserts so each imported profile gets a sequentially
higher priority rather than 0.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`:
- Around line 124-178: The GET handler currently opens assets and runs the
43-entry placeholders replacement on every request (see htmlContent and
placeholders in LocalServerRepositoryImpl); move that work into server
initialization (e.g., in startServer or the class constructor) so the file is
read once, replacements performed once, and the resulting string stored in a
cached property (e.g., cachedIndexHtml) that the GET / handler returns directly;
update the GET handler to use cachedIndexHtml instead of re-reading assets and
looping through placeholders.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt`:
- Around line 494-496: The hardcoded contentDescription strings in Home.kt
(e.g., the conditional contentDescription using "Finish Reordering" with
isReordering, and the "Move Up"/"Move Down" captions) break i18n/accessibility;
replace those literal strings with stringResource lookups (e.g., use
stringResource(R.string.finish_reordering) for the isReordering branch and
stringResource(R.string.move_up) / stringResource(R.string.move_down) for the
move buttons), and add corresponding entries finish_reordering, move_up, and
move_down to your strings.xml so screen readers and translations receive the
localized text; keep the existing ternary/conditional logic and icon/tint usage
(TablerIcons.Check, TablerIcons.ArrowsSort) intact.

In `@app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt`:
- Around line 86-91: Initialize the profiles grid state so the app lands on it
by default: change the initial value of the MutableStateFlow _showProfilesGrid
in AccountViewModel (the backing property for showProfilesGrid) from false to
true, or ensure setShowProfilesGrid(true) is invoked during the ViewModel's
initialization (e.g., in the init block) so the profiles grid is shown before
the first frame; update only the _showProfilesGrid declaration or add the init
call in AccountViewModel.kt.
- Around line 859-893: The moveProfileUp and moveProfileDown flows call
normalizePriorities(), then read profiles via
linkRepository.getAllProfiles().executeAsList() and perform two
updateProfilePriority() calls outside any atomic boundary, allowing concurrent
coroutines to interleave and corrupt priorities; fix by wrapping the entire
normalize-and-swap sequence for a given operation in a single serialized
boundary — either execute a DB transaction that includes normalizePriorities(),
the read, and both updateProfilePriority() calls (using explicit BEGIN/COMMIT on
the SQL driver) or protect the sequence with a shared Mutex / single-threaded
CoroutineDispatcher so moveProfileUp, moveProfileDown and normalizePriorities
cannot run concurrently.

In `@app/src/main/res/values-it/strings.xml`:
- Line 256: The string resource named "reorder" uses a non-standard calque
"Reordina"; update the value for string name="reorder" to the correct Italian
imperative "Riordina" in the strings.xml so the app uses the proper
localization.
- Line 272: Update the two Italian string resources: add a terminal period to
the value of profile_delete_confirmation_with_name so it ends with "verranno
eliminati." to match surrounding confirmation strings, and fix the missing
elision apostrophe in web_error_add_link by changing "Errore nell aggiunta" to
"Errore nell'aggiunta". Ensure you edit the string values for the resource names
profile_delete_confirmation_with_name and web_error_add_link accordingly.

In `@app/src/main/res/values/strings.xml`:
- Around line 271-273: Remove the unused duplicate string resource
delete_profile_confirmation across all localization files (it is identical to
profile_delete_confirmation and unused), update
profile_delete_confirmation_with_name to end with a period, and make the
substitution consistent by quoting the %s placeholder (use the same quoting
style as delete_link_confirmation_with_name, e.g. wrap %s in single quotes) so
its text matches neighboring strings; change only the string resource entries
(profile_delete_confirmation_with_name and removal of
delete_profile_confirmation) and apply the same edits to every
values-*/localized XML file.

In `@app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq`:
- Around line 80-82: The WHERE clause filters out non-matching Tag rows before
aggregation, causing GROUP_CONCAT(Tags.name) and GROUP_CONCAT(Tags.id) to miss a
link's other tags; change the tag-search predicate (Tags.name LIKE '%' || ? ||
'%') so it is used in an EXISTS correlated subquery (e.g., EXISTS(SELECT 1 FROM
Tags t WHERE t.linkId = Deepr.linkId AND t.name LIKE '%' || ? || '%')) while
leaving the LEFT JOIN to Tags intact for aggregation, preserving all joined tag
rows for GROUP_CONCAT; ensure you keep the same number/order of bind parameters
used by the current query so the Kotlin API surface remains unchanged.

In `@README.md`:
- Around line 35-39: Update the markdown table rows to fix two typos: change the
string "Github" in the column header/cell to the correct brand casing "GitHub",
and correct the feature label "Profile speicific themes" to "Profile specific
themes" so the table reads exactly "GitHub" and "Profile specific themes" where
those exact strings appear.

---

Outside diff comments:
In `@app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt`:
- Around line 200-233: The clipboard listener calls clipboard.primaryClip
unconditionally which triggers Android 12+ clipboard-access toasts; change both
the listener body and the initial check in the DisposableEffect to first call
clipboard.primaryClipDescription (or getPrimaryClipDescription()) and verify the
MIME type is text/plain (or hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN))
before calling clipboard.primaryClip/getItemAt(0); keep using normalizeLink,
isValidDeeplink and ClipboardLink as before, and ensure you only call
clipboard.primaryClip when the description indicates text to avoid spurious
toasts.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`:
- Around line 309-347: The GET /api/tags handler is performing an N+1 query by
calling deeprQueries.getLinksAndTags(...) for each tag and hardcodes profileId =
1L; replace that loop with the optimized single-query method
deeprQueries.getAllTagsWithCount(profileId) to return tags with counts in one
call, and obtain the correct profileId from the request/context (instead of 1L).
Map the results of getAllTagsWithCount(...) to TagResponse(id, name, count) and
respond with that list; remove the per-tag getLinksAndTags(...) calls and their
.size usage to eliminate the N+1 issue and ensure counts respect the actual
profileId.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`:
- Around line 176-195: The parsing of comma-separated tag IDs in the
LaunchedEffect (inside AddLinkScreen.kt) is unsafe because tagId.trim().toLong()
can throw; update the block that builds existingTags to use safe parsing (e.g.,
use toLongOrNull() and mapIndexedNotNull) so invalid or empty IDs are skipped,
while still pairing each successful Long with its corresponding name from
selectedLink.tagsNames?.split(",")?.getOrNull(index)?.trim() (falling back to
context.getString(R.string.unknown) when name is missing); then populate
selectedTags and initialSelectedTags with the filtered Tags objects to avoid
exceptions on malformed tagsIds.
- Around line 833-909: The button handlers use the raw deeprInfo.link and call
deeprQueries.getDeeprByLink(...) directly, but save() first normalizes the link
via normalizeLink() and uses executeAsOneOrNull(), causing inconsistent
duplicate detection and duplicated logic; create a single helper e.g.
validateAndSave(execute: Boolean) that calls normalizeLink(deeprInfo.link), then
queries deeprQueries.getDeeprByLink(normalizedLink) using the same existence
check as save (executeAsOneOrNull or a consistent API), sets isError on invalid
links, shows the duplicate Toast when found, and calls save(execute) (or
openDeeplink for execute-only flows) as appropriate; replace the three onClick
blocks (the Button and two FilledTonalButton handlers referencing
deeprInfo.link, save(true)/save(false), and openDeeplink) to call
validateAndSave(true/false) so normalization and duplicate-checking are
centralized and consistent.

In `@app/src/main/res/values/strings.xml`:
- Line 267: Remove the unused string resource named
profile_name_cannot_be_empty: delete its entry from the default strings.xml and
the corresponding translation in values-it/strings.xml, and verify there are no
references (grep/search for "profile_name_cannot_be_empty") before committing;
keep the active string profile_name_cannot_be_blank intact.

---

Nitpick comments:
In `@app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt`:
- Line 200: File uses androidx.compose.runtime.DisposableEffect with a
fully-qualified name and still has an unused import for LaunchedEffect; replace
the fully-qualified call with a proper import and remove the stale import. Add
"import androidx.compose.runtime.DisposableEffect" to the imports, remove
"import androidx.compose.runtime.LaunchedEffect" if unused, and update any
usages (e.g., the call currently written as
androidx.compose.runtime.DisposableEffect(Unit) { ... }) to use the imported
DisposableEffect symbol so imports are consistent and no unused LaunchedEffect
remains.
- Around line 203-226: The clipboard-reading logic is duplicated between the
ClipboardManager.OnPrimaryClipChangedListener block and the initial check;
extract that shared code into a single helper (either a private function or a
lambda) — e.g., create a processClipboard() function that reads
clipboard.primaryClip, gets text, calls normalizeLink(text), checks
isValidDeeplink(normalizedLink) and assigns clipboardLink =
ClipboardLink(normalizedLink) — then call that helper from the listener body and
from the initial check so ClipboardManager.OnPrimaryClipChangedListener,
normalizeLink, isValidDeeplink, ClipboardLink and clipboard are reused without
duplication.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`:
- Line 107: Remove the redundant local shadowing of the parameter by deleting
the line "val port = port" in LocalServerRepositoryImpl (the local variable
shadows the function parameter named port); just use the existing parameter
"port" directly everywhere in the function (or rename if you intended a
different variable), ensuring no other code relies on that local declaration.
- Around line 386-402: Change the POST handler that currently reads the link id
from call.request.queryParameters in the route "/api/links/increment-count" to
accept the id as a path parameter (e.g. POST "/api/links/{id}/increment-count")
or accept a JSON body with an id, then parse that value instead of using
queryParameters; update the route declaration and replace
call.request.queryParameters["id"]?.toLongOrNull() with code that retrieves the
id from call.parameters["id"] or from the request body, and continue calling
accountViewModel.incrementOpenedCount(id) and responding with
SuccessResponse/ErrorResponse as before.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/components/NoteViewDialog.kt`:
- Around line 91-104: Replace the deprecated ClickableText usage by building
annotatedString with LinkAnnotation.Clickable via withLink() in
buildAnnotatedString and render it with BasicText; update the annotation
creation code where you currently use StringAnnotation("URL", ...) so it uses
withLink(url) / LinkAnnotation.Clickable and attach a LinkInteractionListener
that calls openDeeplink(context, url) (preserve the existing annotatedString
variable name and the openDeeplink dispatch logic), then replace the
ClickableText block (which performs getStringAnnotations and offset lookup) with
a BasicText that uses the new annotatedString and the LinkAnnotation.Clickable
listener to handle taps.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`:
- Around line 139-145: The LaunchedEffect block starts new work on
deeprInfo.link but calls a non-cancellable callback (fetchMetadata /
viewModel.fetchMetaData), causing stale responses to overwrite newer state;
change the implementation so the metadata fetch is cancellable or ignores stale
results: either convert fetchMetadata (and underlying viewModel.fetchMetaData)
into a suspend function and call it directly inside the LaunchedEffect coroutine
(so coroutine cancellation will abort in-flight work), or add a request token
(capture the current normalized link or a UUID in LaunchedEffect) and have the
fetch callback verify that the token/link still matches before applying results
(compare against deeprInfo.link or the captured token in the callback). Ensure
you update references to normalizeLink, isValidDeeplink, fetchMetadata, and
viewModel.fetchMetaData accordingly so only the latest request can update UI.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt`:
- Around line 1596-1607: The annotated-string construction is fragile because
split(profile.name) can incorrectly fragment translations or handle multiple
occurrences; instead, find the first occurrence index of profile.name in the
formatted fullMessage (use fullMessage.indexOf(profile.name)) and if index >= 0
build the AnnotatedString by taking fullMessage.substring(0, index), append the
bold profile.name, then append fullMessage.substring(index +
profile.name.length), otherwise fall back to appending fullMessage; update the
logic around fullMessage, parts, and annotatedString in Home.kt to use this
index-based splitting so only the intended occurrence is made bold.
- Around line 1688-1690: The Save button's enabled check uses raw newName and
profile.name, allowing only-whitespace differences to enable the button but
failing later because onClick trims; update the enabled logic to compare trimmed
values instead (use newName.trim().isNotBlank() and compare newName.trim() with
profile.name.trim() or the canonical stored name) so the enabled state matches
the onClick trimming behavior; locate the Save button enabled assignment
(enabled = newName.isNotBlank() && newName != profile.name) and change it to use
trimmed comparisons consistent with the onClick handler.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt`:
- Around line 692-709: The current construction in TagSelectionScreen.kt builds
annotatedString by splitting fullMessage with tag.name (variables fullMessage,
tag.name, parts, annotatedString), which drops content after the second
occurrence; instead find the first occurrence using indexOf(tag.name), then
append substring before, append the tag name with bold SpanStyle, and append the
remainder (substring after first match) so any further occurrences are
preserved; update buildAnnotatedString usage accordingly and keep the Text(...)
rendering as-is.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt`:
- Line 45: Replace the fully-qualified QrCodeColors usages with a simple import:
add an import for QrCodeColors at the top of the file and update all call-sites
(the places where QrCodeView is used and the occurrences around the previous
lines 323-327) to reference QrCodeColors directly instead of its fully-qualified
name; this keeps the import style consistent with QrCodeView and improves
readability.

In `@app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt`:
- Around line 849-857: The current insertProfile in AccountViewModel uses
linkRepository.getMaxPriority() and then linkRepository.insertProfile(name,
maxPriority + 1) separately, which is not atomic and can cause duplicate
priorities under concurrency; change this by adding a single atomic
repository/DAO operation (e.g.,
linkRepository.insertProfileWithNextPriority(name) or a DAO method annotated
with `@Transaction`) that reads the current max priority and inserts the new
profile inside the same transaction, then call that new method from
AccountViewModel.insertProfile instead of calling getMaxPriority and
insertProfile separately; alternatively, if you prefer in-memory serialization,
guard the two calls with a dedicated Mutex/synchronized block in the repository
so the read+insert cannot interleave.
- Around line 141-148: normalizePriorities() currently issues updates for every
profile on init and before moves; change it to detect whether normalization is
required and only perform writes when priorities are out of order or a "dirty"
flag is set. Specifically, in normalizePriorities() (and where it is called)
first load profiles with linkRepository.getAllProfiles().executeAsList() and
check if any profile.priority != index.toLong()—if none mismatch, return early;
alternatively, add a persistent or in-memory dirty flag that is set when
profiles are added/removed/moved (e.g., in your move/swap methods) and only run
updateProfilePriority(profile.id, index.toLong()) when the flag is true, then
clear the flag after successful normalization. Ensure callers (init and move)
use the new check/flag to avoid unconditional DB writes.

In `@app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq`:
- Around line 218-220: Remove the stray blank line between the GROUP BY clause
and the ORDER BY clause so the query remains consistent with others in the file;
specifically, join "GROUP BY Tags.id, Tags.name" and "ORDER BY linkCount DESC,
Tags.name" with no blank line between them (the relevant symbols to locate are
GROUP BY, Tags.id, Tags.name, ORDER BY, and linkCount).

val profile = deeprQueries.getProfileByName(it).executeAsOneOrNull()
if (profile == null) {
deeprQueries.insertProfile(it)
deeprQueries.insertProfile(it, 0L)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "maxPriority|insertProfile" --type=kotlin -A 2 -B 1

Repository: yogeshpaliyal/Deepr

Length of output: 6312


🏁 Script executed:

rg -n "maxPriority\|insertProfile" --include="*.sq" -A 2 -B 1

Repository: yogeshpaliyal/Deepr

Length of output: 504


🏁 Script executed:

# Also check the CsvBookmarkImporter.kt file directly to see the context around line 71
fd "CsvBookmarkImporter.kt" --type=f -x cat -n {}

Repository: yogeshpaliyal/Deepr

Length of output: 7603


Imported profiles all receive priority = 0, producing indeterminate grid ordering.

When a CSV contains multiple new profiles, they are all inserted with priority = 0L at line 71. Since the profile grid orders by priority, imported profiles with identical priority values will appear in undefined order relative to each other.

The codebase already follows the correct pattern elsewhere (see AccountViewModel.insertProfile() which queries maxPriority and assigns maxPriority + 1). Apply the same approach here—query maxPriority for each new profile within the transaction loop and assign sequential priorities.

💡 Suggested approach
 if (profile == null) {
-    deeprQueries.insertProfile(it, 0L)
+    val nextPriority = (deeprQueries.maxPriority().executeAsOneOrNull()?.MAX ?: 0L) + 1L
+    deeprQueries.insertProfile(it, nextPriority)
     deeprQueries.lastInsertRowId().executeAsOneOrNull()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt`
at line 71, Imported profiles are all saved with priority = 0L at the
deeprQueries.insertProfile(it, 0L) call in CsvBookmarkImporter, causing
indeterminate ordering; modify the import transaction so it queries the current
max priority (using the same pattern as AccountViewModel.insertProfile(), i.e.,
call deeprQueries.maxPriority() or equivalent) before inserting each new
profile, set the profile's priority to maxPriority + 1, and increment that in
the loop for subsequent inserts so each imported profile gets a sequentially
higher priority rather than 0.

Comment on lines +124 to +178
var htmlContent =
context.assets
.open("index.html")
.bufferedReader()
.use { it.readText() }

// Replace placeholders with translations
val placeholders =
mapOf(
"{{WEB_TITLE}}" to R.string.web_title,
"{{WEB_SUBTITLE}}" to R.string.web_subtitle,
"{{WEB_PROFILE_LABEL}}" to R.string.web_profile_label,
"{{WEB_NEW_PROFILE}}" to R.string.web_new_profile,
"{{WEB_ADD_NEW_LINK}}" to R.string.web_add_new_link,
"{{WEB_LINK_URL}}" to R.string.web_link_url,
"{{WEB_LINK_NAME}}" to R.string.web_link_name,
"{{WEB_OPTIONAL}}" to R.string.web_optional,
"{{WEB_NOTES_OPTIONAL}}" to R.string.web_notes_optional,
"{{WEB_TAGS_OPTIONAL}}" to R.string.web_tags_optional,
"{{WEB_ADD_LINK_BUTTON}}" to R.string.web_add_link_button,
"{{WEB_YOUR_LINKS}}" to R.string.web_your_links,
"{{WEB_REFRESH}}" to R.string.web_refresh,
"{{WEB_SEARCH_PLACEHOLDER}}" to R.string.web_search_placeholder,
"{{WEB_ALL_TAGS}}" to R.string.web_all_tags,
"{{WEB_SORT_DATE}}" to R.string.web_sort_date,
"{{WEB_SORT_NAME}}" to R.string.web_sort_name,
"{{WEB_SORT_OPENS}}" to R.string.web_sort_opens,
"{{WEB_CLEAR}}" to R.string.web_clear,
"{{WEB_LOADING_LINKS}}" to R.string.web_loading_links,
"{{WEB_LOADING_DESCRIPTION}}" to R.string.web_loading_description,
"{{WEB_CREATE_PROFILE_TITLE}}" to R.string.web_create_profile_title,
"{{WEB_PROFILE_NAME_LABEL}}" to R.string.web_profile_name_label,
"{{WEB_CANCEL}}" to R.string.web_cancel,
"{{WEB_CREATE_PROFILE_BUTTON}}" to R.string.web_create_profile_button,
"{{WEB_STATUS_CREATING}}" to R.string.web_status_creating,
"{{WEB_STATUS_ADDED}}" to R.string.web_status_added,
"{{WEB_SUCCESS_LINK_ADDED}}" to R.string.web_success_link_added,
"{{WEB_ERROR_ADD_LINK}}" to R.string.web_error_add_link,
"{{WEB_ERROR_LOADING}}" to R.string.web_error_loading,
"{{WEB_ERROR_REFRESH}}" to R.string.web_error_refresh,
"{{WEB_ERROR_FETCH}}" to R.string.web_error_fetch,
"{{WEB_MSG_SWITCHED}}" to R.string.web_msg_switched,
"{{WEB_MSG_FILTERED}}" to R.string.web_msg_filtered,
"{{WEB_MSG_FILTERS_CLEARED}}" to R.string.web_msg_filters_cleared,
"{{WEB_NO_LINKS}}" to R.string.web_no_links,
"{{WEB_NO_LINKS_DESCRIPTION}}" to R.string.web_no_links_description,
"{{WEB_NO_MATCHING}}" to R.string.web_no_matching,
"{{WEB_NO_MATCHING_DESCRIPTION}}" to R.string.web_no_matching_description,
"{{WEB_OPEN_LINK}}" to R.string.web_open_link,
"{{WEB_OPENS}}" to R.string.web_opens,
)

placeholders.forEach { (placeholder, resId) ->
htmlContent = htmlContent.replace(placeholder, context.getString(resId))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HTML asset is re-read and all 43 placeholders replaced on every GET / request

The full file I/O (assets.open("index.html")) and the 43-entry forEach replacement loop run on every web page load. Since the content doesn't change during the server's lifetime, this work should be done once at server startup and the result cached.

♻️ Proposed refactor
+    // Cache the localized HTML content at server start
+    private var cachedHtmlContent: String? = null
+
+    private fun buildLocalizedHtml(): String {
+        val html = context.assets.open("index.html").bufferedReader().use { it.readText() }
+        val placeholders = mapOf(
+            "{{WEB_TITLE}}" to R.string.web_title,
+            // ... (same map as today)
+        )
+        return placeholders.entries.fold(html) { acc, (key, resId) ->
+            acc.replace(key, context.getString(resId))
+        }
+    }

Then in startServer:

+    cachedHtmlContent = buildLocalizedHtml()
     server = embeddedServer(CIO, host = "0.0.0.0", port = port) {
         ...
         get("/") {
             try {
-                var htmlContent = context.assets.open("index.html")...
-                placeholders.forEach { ... }
+                val htmlContent = cachedHtmlContent ?: buildLocalizedHtml()
                 call.respondText(htmlContent, ContentType.Text.Html)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt`
around lines 124 - 178, The GET handler currently opens assets and runs the
43-entry placeholders replacement on every request (see htmlContent and
placeholders in LocalServerRepositoryImpl); move that work into server
initialization (e.g., in startServer or the class constructor) so the file is
read once, replacements performed once, and the resulting string stored in a
cached property (e.g., cachedIndexHtml) that the GET / handler returns directly;
update the GET handler to use cachedIndexHtml instead of re-reading assets and
looping through placeholders.

Comment on lines +494 to +496
imageVector = if (isReordering) TablerIcons.Check else TablerIcons.ArrowsSort,
contentDescription = if (isReordering) "Finish Reordering" else stringResource(R.string.reorder),
tint = if (isReordering) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded English content descriptions break i18n/accessibility.

"Finish Reordering" (line 495), "Move Up" (line 899), and "Move Down" (line 913) are hardcoded strings while other UI text correctly uses stringResource(). These should use string resources for proper localization and screen-reader support.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt` around
lines 494 - 496, The hardcoded contentDescription strings in Home.kt (e.g., the
conditional contentDescription using "Finish Reordering" with isReordering, and
the "Move Up"/"Move Down" captions) break i18n/accessibility; replace those
literal strings with stringResource lookups (e.g., use
stringResource(R.string.finish_reordering) for the isReordering branch and
stringResource(R.string.move_up) / stringResource(R.string.move_down) for the
move buttons), and add corresponding entries finish_reordering, move_up, and
move_down to your strings.xml so screen readers and translations receive the
localized text; keep the existing ternary/conditional logic and icon/tint usage
(TablerIcons.Check, TablerIcons.ArrowsSort) intact.

Comment on lines +86 to +91
private val _showProfilesGrid = MutableStateFlow(false)
val showProfilesGrid: StateFlow<Boolean> = _showProfilesGrid

fun setShowProfilesGrid(show: Boolean) {
_showProfilesGrid.value = show
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

_showProfilesGrid defaults to false, but the PR objective states the profiles grid should be the main landing view.

The PR description says "Replaces the default Home screen with a profiles grid as the main landing view," yet _showProfilesGrid is initialized to false, meaning users will land on the links/content view instead of the profiles grid on app start. Either initialize to true or ensure it's set before the first frame.

Proposed fix
-    private val _showProfilesGrid = MutableStateFlow(false)
+    private val _showProfilesGrid = MutableStateFlow(true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private val _showProfilesGrid = MutableStateFlow(false)
val showProfilesGrid: StateFlow<Boolean> = _showProfilesGrid
fun setShowProfilesGrid(show: Boolean) {
_showProfilesGrid.value = show
}
private val _showProfilesGrid = MutableStateFlow(true)
val showProfilesGrid: StateFlow<Boolean> = _showProfilesGrid
fun setShowProfilesGrid(show: Boolean) {
_showProfilesGrid.value = show
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt`
around lines 86 - 91, Initialize the profiles grid state so the app lands on it
by default: change the initial value of the MutableStateFlow _showProfilesGrid
in AccountViewModel (the backing property for showProfilesGrid) from false to
true, or ensure setShowProfilesGrid(true) is invoked during the ViewModel's
initialization (e.g., in the init block) so the profiles grid is shown before
the first frame; update only the _showProfilesGrid declaration or add the init
call in AccountViewModel.kt.

Comment on lines +859 to +893
fun moveProfileUp(profileId: Long) {
viewModelScope.launch(Dispatchers.IO) {
// First, ensure all profiles have unique, sequential priorities
normalizePriorities()

val profiles = linkRepository.getAllProfiles().executeAsList()
val index = profiles.indexOfFirst { it.id == profileId }
if (index > 0) {
val currentProfile = profiles[index]
val previousProfile = profiles[index - 1]

val tempPriority = currentProfile.priority
linkRepository.updateProfilePriority(currentProfile.id, previousProfile.priority)
linkRepository.updateProfilePriority(previousProfile.id, tempPriority)
}
}
}

fun moveProfileDown(profileId: Long) {
viewModelScope.launch(Dispatchers.IO) {
// First, ensure all profiles have unique, sequential priorities
normalizePriorities()

val profiles = linkRepository.getAllProfiles().executeAsList()
val index = profiles.indexOfFirst { it.id == profileId }
if (index != -1 && index < profiles.size - 1) {
val currentProfile = profiles[index]
val nextProfile = profiles[index + 1]

val tempPriority = currentProfile.priority
linkRepository.updateProfilePriority(currentProfile.id, nextProfile.priority)
linkRepository.updateProfilePriority(nextProfile.id, tempPriority)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check database configuration and transaction support
find . -type f -name "*.kt" | xargs grep -l "Room\|Database\|Transaction" | head -20

Repository: yogeshpaliyal/Deepr

Length of output: 206


🏁 Script executed:

# Look at the repository implementation for these methods
rg -n "updateProfilePriority|getAllProfiles|normalizePriorities" -A 5 -B 2 --type=kt

Repository: yogeshpaliyal/Deepr

Length of output: 89


🏁 Script executed:

# Check for existing transaction patterns in the codebase
rg -n "withTransaction|transaction\(" --type=kt -C 2

Repository: yogeshpaliyal/Deepr

Length of output: 89


🏁 Script executed:

# Examine the full AccountViewModel file to understand context
wc -l app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt

Repository: yogeshpaliyal/Deepr

Length of output: 138


🏁 Script executed:

# Fix the file type for Kotlin and search for repository methods
rg -n "updateProfilePriority|getAllProfiles|normalizePriorities" -A 5 -B 2 --type=kotlin

Repository: yogeshpaliyal/Deepr

Length of output: 13414


🏁 Script executed:

# Search for transaction patterns with correct syntax
rg -n "withTransaction|transaction\(" --type=kotlin -C 2

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look at the repository files to understand the database setup
find . -type f -name "*Repository*.kt" | head -10

Repository: yogeshpaliyal/Deepr

Length of output: 785


🏁 Script executed:

# Examine the Room database setup
rg -n "@Database|@Entity|@Dao" --type=kotlin -C 1

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Check the database implementation file
cat -n app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt | head -100

Repository: yogeshpaliyal/Deepr

Length of output: 4508


🏁 Script executed:

# Find database configuration files
find . -type f \( -name "*.sqldelight" -o -name "*Database*.kt" -o -name "*Deepr*.kt" \) | grep -E "(database|queries|sqldelight)" | head -20

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look for the DeeprQueries interface definition
rg -n "interface DeeprQueries|class DeeprQueries" -A 20

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Check if this is SQLiteDelight by looking for .sqldelight files
find . -type f -name "*.sqldelight"

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look for database initialization
rg -n "DatabaseDriverFactory|DatabaseDriver|createDriver" -B 2 -A 5

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Check for any transaction usage in the codebase
rg -n "\.transactionWithResult|\.transaction\(|withTransaction"

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look at the DeeprApplication.kt which was found earlier
cat -n app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt | head -150

Repository: yogeshpaliyal/Deepr

Length of output: 6954


🏁 Script executed:

# Search more broadly for database setup
rg -n "Database\|SqlDelight\|Room\|sqlite" -i | head -50

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Check the imports in LinkRepositoryImpl to understand the DB setup
cat -n app/src/main/java/com/yogeshpaliyal/deepr/data/LinkRepositoryImpl.kt | head -80

Repository: yogeshpaliyal/Deepr

Length of output: 3105


🏁 Script executed:

# Look for gradle files to see dependencies
find . -name "build.gradle*" -o -name "*.gradle.kts" | head -5

Repository: yogeshpaliyal/Deepr

Length of output: 126


🏁 Script executed:

# Check for transaction support in SQLiteDelight
rg -n "transactionWithResult|transactionWithoutResult"

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look at the build.gradle to see SQLiteDelight version
cat -n app/build.gradle.kts | head -100

Repository: yogeshpaliyal/Deepr

Length of output: 3415


🏁 Script executed:

# Search for database.transaction or db.transaction patterns
rg -n "database\.|db\." | grep -i transaction

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Check the DeeprDB class to see available transaction methods
rg -n "class DeeprDB" -A 30

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look at how other database operations handle concurrency
rg -n "launch\(Dispatchers.IO\)" -B 2 -A 10 | head -100

Repository: yogeshpaliyal/Deepr

Length of output: 10530


🏁 Script executed:

# Find the DeeprDB file or generated code
find . -type f -name "DeeprDB.kt" -o -name "*DB.kt" | grep -i deepr

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Check if there's a .sqldelight file
find . -type f -name "*.sqldelight"

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Look at the full LinkRepositoryImpl to understand the complete picture
cat -n app/src/main/java/com/yogeshpaliyal/deepr/data/LinkRepositoryImpl.kt

Repository: yogeshpaliyal/Deepr

Length of output: 9256


Priority swap operations lack atomicity — concurrent reorders can corrupt or duplicate priorities.

moveProfileUp and moveProfileDown perform normalizePriorities() (multiple writes), a fresh getAllProfiles().executeAsList(), then two updateProfilePriority calls in separate viewModelScope.launch(Dispatchers.IO) coroutines without synchronization. If the user taps move buttons quickly, these can interleave mid-execution. For example, normalizePriorities() on coroutine A could write while coroutine B's getAllProfiles() reads stale data, leading to duplicate or inconsistent priorities.

SQLiteDelight does not provide a high-level withTransaction API. Wrap the entire normalize-and-swap logic in a database transaction (via direct SQL BEGIN/COMMIT), or serialize these operations with a mutex or single-threaded dispatcher to prevent concurrent reorders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt`
around lines 859 - 893, The moveProfileUp and moveProfileDown flows call
normalizePriorities(), then read profiles via
linkRepository.getAllProfiles().executeAsList() and perform two
updateProfilePriority() calls outside any atomic boundary, allowing concurrent
coroutines to interleave and corrupt priorities; fix by wrapping the entire
normalize-and-swap sequence for a given operation in a single serialized
boundary — either execute a DB transaction that includes normalizePriorities(),
the read, and both updateProfilePriority() calls (using explicit BEGIN/COMMIT on
the SQL driver) or protect the sequence with a shared Mutex / single-threaded
CoroutineDispatcher so moveProfileUp, moveProfileDown and normalizePriorities
cannot run concurrently.


<!-- Profiles -->
<string name="profiles">Profili</string>
<string name="reorder">Reordina</string>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Non-standard Italian word for "reorder"

"Reordina" is an English calque. The standard Italian imperative/command form of riordinare is "Riordina".

✏️ Proposed fix
-    <string name="reorder">Reordina</string>
+    <string name="reorder">Riordina</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="reorder">Reordina</string>
<string name="reorder">Riordina</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values-it/strings.xml` at line 256, The string resource
named "reorder" uses a non-standard calque "Reordina"; update the value for
string name="reorder" to the correct Italian imperative "Riordina" in the
strings.xml so the app uses the proper localization.

<string name="failed_to_update_profile">Impossibile aggiornare il profilo</string>
<string name="cannot_delete_only_profile">Impossibile eliminare l\'unico profilo esistente</string>
<string name="profile_delete_confirmation">Sei sicuro di voler eliminare questo profilo? Tutti i link e le etichette in questo profilo verranno eliminati.</string>
<string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati</string>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing terminal period and missing apostrophe in web_error_add_link

Two punctuation issues in this file:

  1. Line 272profile_delete_confirmation_with_name ends with "verranno eliminati" without a period, inconsistent with the surrounding confirmation strings (lines 271, 273) that end with .
  2. Line 326web_error_add_link: "Errore nell aggiunta" is missing the elision apostrophe; must be "Errore nell'aggiunta".
✏️ Proposed fixes
-    <string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati</string>
+    <string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati.</string>
-    <string name="web_error_add_link">Errore nell aggiunta del link. Riprova.</string>
+    <string name="web_error_add_link">Errore nell'aggiunta del link. Riprova.</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati</string>
<string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati.</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values-it/strings.xml` at line 272, Update the two Italian
string resources: add a terminal period to the value of
profile_delete_confirmation_with_name so it ends with "verranno eliminati." to
match surrounding confirmation strings, and fix the missing elision apostrophe
in web_error_add_link by changing "Errore nell aggiunta" to "Errore
nell'aggiunta". Ensure you edit the string values for the resource names
profile_delete_confirmation_with_name and web_error_add_link accordingly.

Comment on lines 271 to +273
<string name="profile_delete_confirmation">Are you sure you want to delete this profile? All links and tags in this profile will be deleted.</string>
<string name="profile_delete_confirmation_with_name">Are you sure you want to delete %s profile? All links and tags in this profile will be deleted</string>
<string name="delete_profile_confirmation">Are you sure you want to delete this profile? All links and tags in this profile will be deleted.</string>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the actual content of the strings.xml file at the specified lines
cat -n app/src/main/res/values/strings.xml | sed -n '270,275p'

Repository: yogeshpaliyal/Deepr

Length of output: 777


🏁 Script executed:

# Search for all usages of the potentially duplicate keys across the codebase
rg -n "profile_delete_confirmation|delete_profile_confirmation" --type=kt --type=xml --type=java

Repository: yogeshpaliyal/Deepr

Length of output: 89


🏁 Script executed:

# Also check for the quoted vs unquoted %s pattern in other confirmation strings
rg -n "delete_.*_confirmation|profile_.*_confirmation" app/src/main/res/values/strings.xml | head -20

Repository: yogeshpaliyal/Deepr

Length of output: 851


🏁 Script executed:

# Search for usages of the two potentially duplicate keys in all files
rg "profile_delete_confirmation|delete_profile_confirmation" --type=java --type=xml

Repository: yogeshpaliyal/Deepr

Length of output: 1658


🏁 Script executed:

# Also check broader context - search in all code files
find . -type f \( -name "*.java" -o -name "*.kt" -o -name "*.xml" \) -not -path "*/\.*" | xargs rg "profile_delete_confirmation|delete_profile_confirmation" 2>/dev/null | head -30

Repository: yogeshpaliyal/Deepr

Length of output: 2041


🏁 Script executed:

# Check if delete_profile_confirmation is used anywhere in the codebase
rg "delete_profile_confirmation" --type=java --type=xml app/src/main/java/

Repository: yogeshpaliyal/Deepr

Length of output: 45


🏁 Script executed:

# Also verify the exact usages of profile_delete_confirmation and profile_delete_confirmation_with_name
rg "R\.string\.profile_delete_confirmation|R\.string\.delete_profile_confirmation" app/src/main/java/

Repository: yogeshpaliyal/Deepr

Length of output: 425


Remove unused duplicate string key and fix missing period

delete_profile_confirmation (line 273) is identical to profile_delete_confirmation (line 271) and is never referenced in the codebase—it's dead code present across multiple localization files. Remove it.

Additionally, profile_delete_confirmation_with_name (line 272) is missing a terminal period, inconsistent with both neighboring strings that end with ..

The quoting of %s also differs: delete_link_confirmation_with_name quotes the substitution (\'%s\'), while profile_delete_confirmation_with_name does not (%s). Align the quoting style for consistency.

✏️ Proposed fix
-    <string name="profile_delete_confirmation">Are you sure you want to delete this profile? All links and tags in this profile will be deleted.</string>
-    <string name="profile_delete_confirmation_with_name">Are you sure you want to delete %s profile? All links and tags in this profile will be deleted</string>
-    <string name="delete_profile_confirmation">Are you sure you want to delete this profile? All links and tags in this profile will be deleted.</string>
+    <string name="profile_delete_confirmation">Are you sure you want to delete this profile? All links and tags in this profile will be deleted.</string>
+    <string name="profile_delete_confirmation_with_name">Are you sure you want to delete '%s' profile? All links and tags in this profile will be deleted.</string>

Remove delete_profile_confirmation from all localization files (values-*, values-et, values-it, values-hi, etc.).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values/strings.xml` around lines 271 - 273, Remove the
unused duplicate string resource delete_profile_confirmation across all
localization files (it is identical to profile_delete_confirmation and unused),
update profile_delete_confirmation_with_name to end with a period, and make the
substitution consistent by quoting the %s placeholder (use the same quoting
style as delete_link_confirmation_with_name, e.g. wrap %s in single quotes) so
its text matches neighboring strings; change only the string resource entries
(profile_delete_confirmation_with_name and removal of
delete_profile_confirmation) and apply the same edits to every
values-*/localized XML file.

Comment on lines 80 to +82
WHERE
Deepr.profileId = ?
AND (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%' OR Deepr.notes LIKE '%' || ? || '%')
AND (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%' OR Deepr.notes LIKE '%' || ? || '%' OR Tags.name LIKE '%' || ? || '%')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tag search in WHERE with LEFT JOIN causes incomplete GROUP_CONCAT — matched links will display only the searched tag, not all their tags.

With LEFT JOIN Tags producing one row per tag per link, the WHERE filter Tags.name LIKE '%' || ? || '%' eliminates all non-matching tag rows for a link before GROUP BY. As a result, GROUP_CONCAT(Tags.name) in the SELECT only aggregates the surviving (matching) tag rows — every other tag of the same link is silently dropped from tagsNames and tagsIds in the result set.

Concrete example: Link "My App" has tags ["React", "JavaScript"]. User searches "React".

  • Left-join row for "React" passes the WHERE → survives into GROUP BY.
  • Left-join row for "JavaScript" is filtered out.
  • GROUP_CONCAT produces "React" only; "JavaScript" is invisible in the UI for this result.

The fix is to decouple the tag-search predicate from the join used for aggregation by moving it into an EXISTS correlated subquery, so the LEFT JOIN rows remain intact for GROUP_CONCAT.

🐛 Proposed fix — preserve all tag rows via EXISTS
 WHERE
   Deepr.profileId = ?
-  AND (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%' OR Deepr.notes LIKE '%' || ? || '%' OR Tags.name LIKE '%' || ? || '%')
+  AND (
+    Deepr.link LIKE '%' || ? || '%'
+    OR Deepr.name LIKE '%' || ? || '%'
+    OR Deepr.notes LIKE '%' || ? || '%'
+    OR EXISTS (
+      SELECT 1 FROM Tags t2
+      INNER JOIN LinkTags lt2 ON t2.id = lt2.tagId
+      WHERE lt2.linkId = Deepr.id
+        AND t2.name LIKE '%' || ? || '%'
+    )
+  )

The number of ? parameters for the search query stays at 4, so the generated Kotlin API surface is unchanged.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WHERE
Deepr.profileId = ?
AND (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%' OR Deepr.notes LIKE '%' || ? || '%')
AND (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%' OR Deepr.notes LIKE '%' || ? || '%' OR Tags.name LIKE '%' || ? || '%')
WHERE
Deepr.profileId = ?
AND (
Deepr.link LIKE '%' || ? || '%'
OR Deepr.name LIKE '%' || ? || '%'
OR Deepr.notes LIKE '%' || ? || '%'
OR EXISTS (
SELECT 1 FROM Tags t2
INNER JOIN LinkTags lt2 ON t2.id = lt2.tagId
WHERE lt2.linkId = Deepr.id
AND t2.name LIKE '%' || ? || '%'
)
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq` around lines 80 -
82, The WHERE clause filters out non-matching Tag rows before aggregation,
causing GROUP_CONCAT(Tags.name) and GROUP_CONCAT(Tags.id) to miss a link's other
tags; change the tag-search predicate (Tags.name LIKE '%' || ? || '%') so it is
used in an EXISTS correlated subquery (e.g., EXISTS(SELECT 1 FROM Tags t WHERE
t.linkId = Deepr.linkId AND t.name LIKE '%' || ? || '%')) while leaving the LEFT
JOIN to Tags intact for aggregation, preserving all joined tag rows for
GROUP_CONCAT; ensure you keep the same number/order of bind parameters used by
the current query so the Kotlin API surface remains unchanged.

Comment on lines +35 to +39
| Feature | Github Release | F-droid | Play Store | Pro Version on Play Store |
|-----------------------------------|----------------|---------|------------|-----------|
| Firebase Analytics | ❌ | | ✅ | ✅ |
| Google Drive Backup | ❌ | ❌ | ❌ | ✅ |
| Profile speicific themes | ❌ | ❌ | ❌ | ✅ |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Two text errors in the changed table rows.

  1. Line 35/39: "Github" → "GitHub" (official branding; flagged by static analysis).
  2. Line 39: "Profile speicific themes" → "Profile specific themes" (transposed letters).
✏️ Proposed fix
-| Feature                           | Github Release | F-droid | Play Store | Pro Version on Play Store |
-|-----------------------------------|----------------|---------|------------|-----------|
-| Firebase Analytics                | ❌              | ❌       | ✅          | ✅  |
-| Google Drive Backup | ❌              | ❌       | ❌         | ✅  |
-| Profile speicific themes | ❌              | ❌       | ❌          | ✅  |
+| Feature                           | GitHub Release | F-droid | Play Store | Pro Version on Play Store |
+|-----------------------------------|----------------|---------|------------|-----------|
+| Firebase Analytics                | ❌              | ❌       | ✅          | ✅  |
+| Google Drive Backup               | ❌              | ❌       | ❌          | ✅  |
+| Profile specific themes           | ❌              | ❌       | ❌          | ✅  |
🧰 Tools
🪛 LanguageTool

[uncategorized] ~35-~35: The official name of this software platform is spelled with a capital “H”.
Context: ... | Feature | Github Release | F-droid | Play Store | Pro Ve...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 35 - 39, Update the markdown table rows to fix two
typos: change the string "Github" in the column header/cell to the correct brand
casing "GitHub", and correct the feature label "Profile speicific themes" to
"Profile specific themes" so the table reads exactly "GitHub" and "Profile
specific themes" where those exact strings appear.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt (1)

499-499: ⚠️ Potential issue | 🟡 Minor

Localize the remaining reorder content descriptions.

Line [499], Line [927], and Line [941] still use hardcoded English strings ("Finish Reordering", "Move Up", "Move Down"), which breaks localization/accessibility consistency.

Proposed fix
-                                        contentDescription = if (isReordering) "Finish Reordering" else stringResource(R.string.reorder),
+                                        contentDescription =
+                                            if (isReordering) stringResource(R.string.finish_reordering)
+                                            else stringResource(R.string.reorder),
...
-                                            contentDescription = "Move Up",
+                                            contentDescription = stringResource(R.string.move_up),
...
-                                            contentDescription = "Move Down",
+                                            contentDescription = stringResource(R.string.move_down),

Also applies to: 927-927, 941-941

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt` at line
499, The contentDescription values are using hardcoded English strings; update
them to use string resources so they're localizable — replace the literal
"Finish Reordering" in the contentDescription expression that checks
isReordering with stringResource(R.string.finish_reordering) (or equivalent),
and replace "Move Up" and "Move Down" usages with
stringResource(R.string.move_up) and stringResource(R.string.move_down)
respectively; locate these in Home.kt where contentDescription is passed (the
composable using isReordering and the up/down buttons) and ensure the
corresponding string resource keys are added to strings.xml.
app/src/main/res/values-it/strings.xml (2)

257-257: ⚠️ Potential issue | 🟡 Minor

Use the standard Italian imperative for reorder.

Line 257 still uses “Reordina”; the standard UI imperative is “Riordina”.

✏️ Suggested fix
-    <string name="reorder">Reordina</string>
+    <string name="reorder">Riordina</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values-it/strings.xml` at line 257, The string resource
named "reorder" currently uses the non-standard Italian form "Reordina"; update
the value of the resource with name "reorder" to the standard imperative
"Riordina" so the UI uses the correct Italian wording.

273-273: ⚠️ Potential issue | 🟡 Minor

Fix unresolved Italian punctuation/elision issues.

Line 273 is missing the terminal period, and Line 327 should use “nell'aggiunta” (apostrophe required).

✏️ Suggested fix
-    <string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati</string>
+    <string name="profile_delete_confirmation_with_name">Sei sicuro di voler eliminare il profilo %s? Tutti i link e le etichette in questo profilo verranno eliminati.</string>
-    <string name="web_error_add_link">Errore nell aggiunta del link. Riprova.</string>
+    <string name="web_error_add_link">Errore nell'aggiunta del link. Riprova.</string>

Also applies to: 327-327

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values-it/strings.xml` at line 273, The Italian strings have
punctuation and elision issues: add a terminal period to the string with name
profile_delete_confirmation_with_name and fix the other affected string to use
the elided form "nell'aggiunta" (include the apostrophe) — locate the string
resource profile_delete_confirmation_with_name and append a final period, and
locate the other Italian string (line ~327) and replace its word with
"nell'aggiunta" to correct the apostrophe/elision.
app/src/main/res/values/strings.xml (1)

272-273: ⚠️ Potential issue | 🟡 Minor

Clean up profile-delete confirmation strings.

Line 272 is missing terminal punctuation and reads awkwardly; Line 273 duplicates Line 271 and adds localization maintenance overhead.

✏️ Suggested fix
-    <string name="profile_delete_confirmation_with_name">Are you sure you want to delete %s profile? All links and tags in this profile will be deleted</string>
-    <string name="delete_profile_confirmation">Are you sure you want to delete this profile? All links and tags in this profile will be deleted.</string>
+    <string name="profile_delete_confirmation_with_name">Are you sure you want to delete '%s' profile? All links and tags in this profile will be deleted.</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values/strings.xml` around lines 272 - 273, Update the two
string resources to remove duplication and fix punctuation/grammar: eliminate
the redundant delete_profile_confirmation entry and standardize on a single,
well-formed resource (profile_delete_confirmation_with_name) whose text includes
proper terminal punctuation and reads smoothly when a profile name is inserted
(adjust "%s profile" wording to "%s profile?" or better "%s profile." as
appropriate); ensure the final string resource name remains unique
(profile_delete_confirmation_with_name) and update any callers to use that
resource.
🧹 Nitpick comments (1)
app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt (1)

838-842: Avoid O(n²) index lookup in ProfilesGrid.

Line [841] uses profiles.indexOf(profile) inside item rendering; this adds linear lookup per item. Use indexed items directly.

Proposed refactor
+import androidx.compose.foundation.lazy.grid.itemsIndexed
...
-            items(profiles, key = { it.id }) { profile ->
+            itemsIndexed(profiles, key = { _, profile -> profile.id }) { index, profile ->
                 val isSelected = profile.id == currentProfileId
                 val hapticFeedback = LocalHapticFeedback.current
-                val index = profiles.indexOf(profile)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt` around
lines 838 - 842, The code in the items(...) block does an O(n) lookup with
profiles.indexOf(profile) for every item; change the Lazy/Compose items call to
an indexed variant so the index is provided directly (e.g., use itemsIndexed
with key = { index, profile -> profile.id } and the lambda signature (index,
profile) -> ...), then remove the profiles.indexOf(profile) call and use the
provided index; keep existing uses of currentProfileId,
LocalHapticFeedback.current, and isSelected as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`:
- Around line 139-145: The LaunchedEffect tied to deeprInfo.link currently calls
fetchMetadata on every link change (while the user types); modify the effect to
only auto-fetch once for a provided/initial link by adding a one-time guard
(e.g., a remembered Boolean like hasPrefilled or initialPrefillDone) and only
call normalizeLink/isValidDeeplink/fetchMetadata when that flag is false and
deeprInfo.name.isEmpty(), then set the flag true after calling fetchMetadata;
update the LaunchedEffect(deeprInfo.link) block and use normalizeLink,
isValidDeeplink, and fetchMetadata as-is.
- Around line 123-131: The callback from viewModel.fetchMetaData can write stale
metadata if the user changed the link before the async response; capture the
requested URL (e.g., val expectedLink = urlToFetch) before calling fetchMetaData
and inside the callback verify the current deeprInfo.link (or current input)
still equals expectedLink before updating deeprInfo, isNameError, and thumbnail;
only apply the metadata when they match and still relevant, and still set
isFetchingMetadata.value = false regardless.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt`:
- Around line 1594-1663: The delete confirmation AlertDialog is shown while the
Edit Profile dialog is always rendered, causing stacked dialogs; update the
edit-dialog rendering (the block controlled by the edit dialog state, e.g.,
showEditDialog or showEditProfileDialog) to only render when the delete
confirmation is not visible by adding a guard like if (!showDeleteConfirmation
&& showEditDialog) { ... } around the edit dialog UI, and ensure the edit
dialog's onDismiss clears its own flag so both dialogs cannot remain open
simultaneously (references: showDeleteConfirmation, the edit dialog boolean
state, and viewModel.deleteProfile/onDismiss handlers).

In `@app/src/main/res/values/strings.xml`:
- Line 31: The string resource save_your_link_below has a singular/plural
mismatch ("link" vs "them"); update its text to use a consistent singular form
by replacing "them" with "it" so the value reads "Save your link below to
quickly access it later" (modify the string value for save_your_link_below).

---

Duplicate comments:
In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt`:
- Line 499: The contentDescription values are using hardcoded English strings;
update them to use string resources so they're localizable — replace the literal
"Finish Reordering" in the contentDescription expression that checks
isReordering with stringResource(R.string.finish_reordering) (or equivalent),
and replace "Move Up" and "Move Down" usages with
stringResource(R.string.move_up) and stringResource(R.string.move_down)
respectively; locate these in Home.kt where contentDescription is passed (the
composable using isReordering and the up/down buttons) and ensure the
corresponding string resource keys are added to strings.xml.

In `@app/src/main/res/values-it/strings.xml`:
- Line 257: The string resource named "reorder" currently uses the non-standard
Italian form "Reordina"; update the value of the resource with name "reorder" to
the standard imperative "Riordina" so the UI uses the correct Italian wording.
- Line 273: The Italian strings have punctuation and elision issues: add a
terminal period to the string with name profile_delete_confirmation_with_name
and fix the other affected string to use the elided form "nell'aggiunta"
(include the apostrophe) — locate the string resource
profile_delete_confirmation_with_name and append a final period, and locate the
other Italian string (line ~327) and replace its word with "nell'aggiunta" to
correct the apostrophe/elision.

In `@app/src/main/res/values/strings.xml`:
- Around line 272-273: Update the two string resources to remove duplication and
fix punctuation/grammar: eliminate the redundant delete_profile_confirmation
entry and standardize on a single, well-formed resource
(profile_delete_confirmation_with_name) whose text includes proper terminal
punctuation and reads smoothly when a profile name is inserted (adjust "%s
profile" wording to "%s profile?" or better "%s profile." as appropriate);
ensure the final string resource name remains unique
(profile_delete_confirmation_with_name) and update any callers to use that
resource.

---

Nitpick comments:
In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt`:
- Around line 838-842: The code in the items(...) block does an O(n) lookup with
profiles.indexOf(profile) for every item; change the Lazy/Compose items call to
an indexed variant so the index is provided directly (e.g., use itemsIndexed
with key = { index, profile -> profile.id } and the lambda signature (index,
profile) -> ...), then remove the profiles.indexOf(profile) call and use the
provided index; keep existing uses of currentProfileId,
LocalHapticFeedback.current, and isSelected as before.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8e76b5 and 684d9be.

📒 Files selected for processing (6)
  • app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt
  • app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-et/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/res/values-es/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/res/values-et/strings.xml

Comment on lines +123 to +131
val fetchMetadata: (String?) -> Unit = { link ->
val urlToFetch = link ?: deeprInfo.link
if (urlToFetch.isNotBlank()) {
isFetchingMetadata.value = true
viewModel.fetchMetaData(urlToFetch) {
isFetchingMetadata.value = false
if (it != null) {
deeprInfo = deeprInfo.copy(name = it.title ?: "", thumbnail = it.image ?: "")
isNameError = false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against stale metadata responses overwriting current input.

The async callback applies name/thumbnail unconditionally; if the link changes before completion, stale metadata can be written into the new link form state.

✏️ Suggested fix (apply only if response matches current link)
     val fetchMetadata: (String?) -> Unit = { link ->
-        val urlToFetch = link ?: deeprInfo.link
+        val urlToFetch = normalizeLink(link ?: deeprInfo.link)
         if (urlToFetch.isNotBlank()) {
+            val requestLink = urlToFetch
             isFetchingMetadata.value = true
-            viewModel.fetchMetaData(urlToFetch) {
+            viewModel.fetchMetaData(requestLink) {
                 isFetchingMetadata.value = false
-                if (it != null) {
+                val currentLink = normalizeLink(deeprInfo.link)
+                if (it != null && currentLink == requestLink) {
                     deeprInfo = deeprInfo.copy(name = it.title ?: "", thumbnail = it.image ?: "")
                     isNameError = false
                 } else {
                     Toast.makeText(context, fetchMetadataErrorText, Toast.LENGTH_SHORT).show()
                 }
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`
around lines 123 - 131, The callback from viewModel.fetchMetaData can write
stale metadata if the user changed the link before the async response; capture
the requested URL (e.g., val expectedLink = urlToFetch) before calling
fetchMetaData and inside the callback verify the current deeprInfo.link (or
current input) still equals expectedLink before updating deeprInfo, isNameError,
and thumbnail; only apply the metadata when they match and still relevant, and
still set isFetchingMetadata.value = false regardless.

Comment on lines +139 to +145
// Pre-populate and fetch if link is provided (e.g. from long press)
LaunchedEffect(deeprInfo.link) {
val normalized = normalizeLink(deeprInfo.link)
if (isValidDeeplink(normalized) && deeprInfo.name.isEmpty()) {
fetchMetadata(normalized)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid auto-fetching metadata on every link edit.

This effect runs for each deeprInfo.link change and can repeatedly call metadata fetch while the user types.

✏️ Suggested fix (gate to one-time prefill behavior)
+    var hasAutoFetchedMetadata by remember(selectedLink.id) { mutableStateOf(false) }
     // Pre-populate and fetch if link is provided (e.g. from long press)
-    LaunchedEffect(deeprInfo.link) {
+    LaunchedEffect(deeprInfo.link, hasAutoFetchedMetadata, selectedLink.id) {
         val normalized = normalizeLink(deeprInfo.link)
-        if (isValidDeeplink(normalized) && deeprInfo.name.isEmpty()) {
+        if (
+            !hasAutoFetchedMetadata &&
+            selectedLink.link.isNotBlank() &&
+            isValidDeeplink(normalized) &&
+            deeprInfo.name.isEmpty()
+        ) {
+            hasAutoFetchedMetadata = true
             fetchMetadata(normalized)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/addlink/AddLinkScreen.kt`
around lines 139 - 145, The LaunchedEffect tied to deeprInfo.link currently
calls fetchMetadata on every link change (while the user types); modify the
effect to only auto-fetch once for a provided/initial link by adding a one-time
guard (e.g., a remembered Boolean like hasPrefilled or initialPrefillDone) and
only call normalizeLink/isValidDeeplink/fetchMetadata when that flag is false
and deeprInfo.name.isEmpty(), then set the flag true after calling
fetchMetadata; update the LaunchedEffect(deeprInfo.link) block and use
normalizeLink, isValidDeeplink, and fetchMetadata as-is.

Comment on lines +1594 to +1663
if (showDeleteConfirmation) {
AlertDialog(
onDismissRequest = { showDeleteConfirmation = false },
icon = {
Surface(
color = MaterialTheme.colorScheme.errorContainer,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = TablerIcons.Trash,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(12.dp),
)
}
},
title = {
Text(
text = stringResource(R.string.delete_profile),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
val fullMessage = stringResource(R.string.profile_delete_confirmation_with_name, profile.name)
val parts = fullMessage.split(profile.name)
val annotatedString = buildAnnotatedString {
if (parts.size >= 2) {
append(parts[0])
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(profile.name)
}
append(parts[1])
} else {
append(fullMessage)
}
}
Text(
text = annotatedString,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
Button(
onClick = {
viewModel.deleteProfile(profile.id)
showDeleteConfirmation = false
onDismiss()
Toast.makeText(context, context.getString(R.string.profile_deleted_successfully), Toast.LENGTH_SHORT).show()
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) {
Text(stringResource(R.string.delete))
}
},
dismissButton = {
TextButton(onClick = { showDeleteConfirmation = false }) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent stacked dialogs in profile management flow.

Line [1665] always renders the edit dialog, even when showDeleteConfirmation is true (Line [1594]), so both dialogs can appear together.

Proposed fix
-    if (showDeleteConfirmation) {
+    if (showDeleteConfirmation) {
         AlertDialog(
             onDismissRequest = { showDeleteConfirmation = false },
             ...
         )
-    }
-
-    AlertDialog(
-        onDismissRequest = onDismiss,
-        title = { Text(stringResource(R.string.edit_profile)) },
-        text = {
-            Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
-                OutlinedTextField(
-                    value = newName,
-                    onValueChange = {
-                        newName = it
-                        error = null
-                    },
-                    label = { Text(stringResource(R.string.profile_name)) },
-                    singleLine = true,
-                    isError = error != null,
-                    modifier = Modifier.fillMaxWidth(),
-                    shape = RoundedCornerShape(12.dp),
-                )
-                error?.let {
-                    Text(text = it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
-                }
-            }
-        },
-        confirmButton = {
-            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
-                if (allProfiles.size > 1) {
-                    TextButton(
-                        onClick = {
-                            showDeleteConfirmation = true
-                        },
-                        colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
-                    ) {
-                        Text(stringResource(R.string.delete))
-                    }
-                }
-                
-                Button(
-                    onClick = {
-                        val trimmed = newName.trim()
-                        if (trimmed.isBlank()) {
-                            error = context.getString(R.string.profile_name_cannot_be_blank)
-                            return@Button
-                        }
-                        if (allProfiles.any { it.name.equals(trimmed, ignoreCase = true) && it.id != profile.id }) {
-                            error = context.getString(R.string.profile_name_exists)
-                            return@Button
-                        }
-                        
-                        coroutineScope.launch {
-                            viewModel.updateProfile(profile.id, trimmed, profile.themeMode, profile.colorTheme)
-                            onDismiss()
-                            Toast.makeText(context, context.getString(R.string.profile_updated_successfully), Toast.LENGTH_SHORT).show()
-                        }
-                    },
-                    enabled = newName.isNotBlank() && newName != profile.name
-                ) {
-                    Text(stringResource(R.string.save))
-                }
-            }
-        },
-        dismissButton = {
-            TextButton(onClick = onDismiss) {
-                Text(stringResource(android.R.string.cancel))
-            }
-        }
-    )
+    } else {
+        AlertDialog(
+            onDismissRequest = onDismiss,
+            title = { Text(stringResource(R.string.edit_profile)) },
+            text = {
+                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+                    OutlinedTextField(
+                        value = newName,
+                        onValueChange = {
+                            newName = it
+                            error = null
+                        },
+                        label = { Text(stringResource(R.string.profile_name)) },
+                        singleLine = true,
+                        isError = error != null,
+                        modifier = Modifier.fillMaxWidth(),
+                        shape = RoundedCornerShape(12.dp),
+                    )
+                    error?.let {
+                        Text(text = it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
+                    }
+                }
+            },
+            confirmButton = {
+                Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+                    if (allProfiles.size > 1) {
+                        TextButton(
+                            onClick = { showDeleteConfirmation = true },
+                            colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
+                        ) {
+                            Text(stringResource(R.string.delete))
+                        }
+                    }
+                    Button(
+                        onClick = {
+                            val trimmed = newName.trim()
+                            if (trimmed.isBlank()) {
+                                error = context.getString(R.string.profile_name_cannot_be_blank)
+                                return@Button
+                            }
+                            if (allProfiles.any { it.name.equals(trimmed, ignoreCase = true) && it.id != profile.id }) {
+                                error = context.getString(R.string.profile_name_exists)
+                                return@Button
+                            }
+                            coroutineScope.launch {
+                                viewModel.updateProfile(profile.id, trimmed, profile.themeMode, profile.colorTheme)
+                                onDismiss()
+                                Toast.makeText(context, context.getString(R.string.profile_updated_successfully), Toast.LENGTH_SHORT).show()
+                            }
+                        },
+                        enabled = newName.isNotBlank() && newName != profile.name
+                    ) {
+                        Text(stringResource(R.string.save))
+                    }
+                }
+            },
+            dismissButton = {
+                TextButton(onClick = onDismiss) {
+                    Text(stringResource(android.R.string.cancel))
+                }
+            }
+        )
+    }

Also applies to: 1665-1729

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt` around
lines 1594 - 1663, The delete confirmation AlertDialog is shown while the Edit
Profile dialog is always rendered, causing stacked dialogs; update the
edit-dialog rendering (the block controlled by the edit dialog state, e.g.,
showEditDialog or showEditProfileDialog) to only render when the delete
confirmation is not visible by adding a guard like if (!showDeleteConfirmation
&& showEditDialog) { ... } around the edit dialog UI, and ensure the edit
dialog's onDismiss clears its own flag so both dialogs cannot remain open
simultaneously (references: showDeleteConfirmation, the edit dialog boolean
state, and viewModel.deleteProfile/onDismiss handlers).

<string name="add_tag">Add tag</string>
<string name="no_links_saved_yet">No links saved yet</string>
<string name="save_your_link_below">Save your link below to quickly access them later.</string>
<string name="save_your_link_below">Save your link below to quickly access them later</string>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix singular/plural mismatch in helper text.

Line 31 says “Save your link … access them later”; use singular pronoun for consistency.

✏️ Suggested fix
-    <string name="save_your_link_below">Save your link below to quickly access them later</string>
+    <string name="save_your_link_below">Save your link below to quickly access it later</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="save_your_link_below">Save your link below to quickly access them later</string>
<string name="save_your_link_below">Save your link below to quickly access it later</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values/strings.xml` at line 31, The string resource
save_your_link_below has a singular/plural mismatch ("link" vs "them"); update
its text to use a consistent singular form by replacing "them" with "it" so the
value reads "Save your link below to quickly access it later" (modify the string
value for save_your_link_below).

@ALE-ARME ALE-ARME force-pushed the issue-360-partial branch from 684d9be to a8e76b5 Compare March 1, 2026 09:50
@yogeshpaliyal
Copy link
Copy Markdown
Owner

@ALE-ARME
Please keep small PRs would be easy to review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manually reorder list of profiles Home page improvements

2 participants